(function (root, factory) {
    'use strict';

    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define([], factory);
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory();
    } else {
        // Browser globals (root is window)
        root.emojify = factory();
    }
}(this, function () {
        'use strict';

        var emojify = (function () {
            /**
             * NB!
             * The namedEmojiString variable is updated automatically by the
             * "update" gulp task. Do not remove the comment as this will
             * cause the gulp task to stop working.
             */
            var namedEmojiString =
            /*##EMOJILIST*/"Expression_1,Expression_2,Expression_3,Expression_4,Expression_5,Expression_6,Expression_7,Expression_8,Expression_9,Expression_10,Expression_11,Expression_12,Expression_13,Expression_14,Expression_15,Expression_16,Expression_17,Expression_18,Expression_19,Expression_20,Expression_21,Expression_22,Expression_23,Expression_24,Expression_25,Expression_26,Expression_27,Expression_28,Expression_29,Expression_30,Expression_31,Expression_32,Expression_33,Expression_34,Expression_35,Expression_36,Expression_37,Expression_38,Expression_39,Expression_40,Expression_41,Expression_42,Expression_43,Expression_44,Expression_45,Expression_46,Expression_47,Expression_48,Expression_49,Expression_50,Expression_51,Expression_52,Expression_53,Expression_54,Expression_55,Expression_56,Expression_57,Expression_58,Expression_59,Expression_60,Expression_61,Expression_62,Expression_63,Expression_64,Expression_65,Expression_66,Expression_67,Expression_68,Expression_69,Expression_70,Expression_71,Expression_72,Expression_73,Expression_74,Expression_75,Expression_76,Expression_77,Expression_78,Expression_79,Expression_80,Expression_81,Expression_82,Expression_83,Expression_84,Expression_85,Expression_86,Expression_87,Expression_88,Expression_89,Expression_90,Expression_91,Expression_92,Expression_93,Expression_94,Expression_95,Expression_96,Expression_97,Expression_98,Expression_99,Expression_100";
			var emoticons = {
                Expression_1: "[微笑]",
                Expression_2: "[撇嘴]",
                Expression_3: "[色]",
                Expression_4: "[发呆]",
                Expression_5: "[得意]",
                Expression_6: "[流泪]",
                Expression_7: "[害羞]",
                Expression_8: "[闭嘴]",
                Expression_9: "[睡]",
                Expression_10: "[大哭]",
                Expression_11: "[尴尬]",
                Expression_12: "[发怒]",
                Expression_13: "[调皮]",
                Expression_14: "[呲牙]",
                Expression_15: "[惊讶]",
                Expression_16: "[难过]",
                Expression_17: "[酷]",
                Expression_18: "[冷汗]",
                Expression_19: "[抓狂]",
                Expression_20: "[吐]",
                Expression_21: "[偷笑]",
                Expression_22: "[愉快]",
                Expression_23: "[白眼]",
                Expression_24: "[傲慢]",
                Expression_25: "[饥饿]",
                Expression_26: "[困]",
                Expression_27: "[惊恐]",
                Expression_28: "[流汗]",
                Expression_29: "[憨笑]",
                Expression_30: "[悠闲]",
                Expression_31: "[奋斗]",
                Expression_32: "[咒骂]",
                Expression_33: "[疑问]",
                Expression_34: "[嘘]",
                Expression_35: "[晕]",
                Expression_36: "[疯了]",
                Expression_37: "[衰]",
                Expression_38: "[骷髅]",
                Expression_39: "[敲打]",
                Expression_40: "[再见]",
                Expression_41: "[擦汗]",
                Expression_42: "[抠鼻]",
                Expression_43: "[鼓掌]",
                Expression_44: "[糗大了]",
                Expression_45: "[坏笑]",
                Expression_46: "[左哼哼]",
                Expression_47: "[右哼哼]",
                Expression_48: "[哈欠]",
                Expression_49: "[鄙视]",
                Expression_50: "[委屈]",
                Expression_51: "[快哭了]",
                Expression_52: "[阴险]",
                Expression_53: "[亲亲]",
                Expression_54: "[吓]",
                Expression_55: "[可怜]",
                Expression_56: "[菜刀]",
                Expression_57: "[西瓜]",
                Expression_58: "[啤酒]",
                Expression_59: "[篮球]",
                Expression_60: "[乒乓]",
                Expression_61: "[咖啡]",
                Expression_62: "[饭]",
                Expression_63: "[猪头]",
                Expression_64: "[玫瑰]",
                Expression_65: "[凋谢]",
                Expression_66: "[嘴唇]",
                Expression_67: "[爱心]",
                Expression_68: "[心碎]",
                Expression_69: "[蛋糕]",
                Expression_70: "[闪电]",
                Expression_71: "[炸弹]",
                Expression_72: "[刀]",
                Expression_73: "[足球]",
                Expression_74: "[瓢虫]",
                Expression_75: "[便便]",
                Expression_76: "[月亮]",
                Expression_77: "[太阳]",
                Expression_78: "[礼物]",
                Expression_79: "[拥抱]",
                Expression_80: "[强]",
                Expression_81: "[弱]",
                Expression_82: "[握手]",
                Expression_83: "[胜利]",
                Expression_84: "[抱拳]",
                Expression_85: "[勾引]",
                Expression_86: "[拳头]",
                Expression_87: "[差劲]",
                Expression_88: "[爱你]",
                Expression_89: "[NO]",
                Expression_90: "[OK]",
                Expression_91: "[爱情]",
                Expression_92: "[飞吻]",
                Expression_93: "[跳跳]",
                Expression_94: "[发抖]",
                Expression_95: "[怄火]",
                Expression_96: "[转圈]",
                Expression_97: "[磕头]",
                Expression_98: "[回头]",
                Expression_99: "[跳绳]",
                Expression_100: "[投降]",
            };

            var namedEmoji = namedEmojiString.split(',');

            /* A hash with the named emoji as keys */
            var namedMatchHash = namedEmoji.reduce(function(memo, v) {
                memo[v] = true;
                return memo;
            }, {});

            var emoticonsProcessed;
            var emojiMegaRe;

            function initEmoticonsProcessed() {
             //   if (defaultConfig.ignore_emoticons) {
             //      emoticons = {
             ///* :..: */ named: /:([a-z0-9A-Z_-]+):/,
             ///* :+1: */ thumbsup: /:\+1:/g,
             ///* :-1: */ thumbsdown: /:\-1:/g
             //       };
             //   }

                return Object.keys(emoticons).map(function(key) {
                    return [emoticons[key], key];
                });
            }

            function initMegaRe() {
                /* The source for our mega-regex */
                var mega = emoticonsProcessed
                        .map(function(v) {
                            var re = v[0];
                            var val = re.source || re;
                            val = val.replace(/(^|[^\[])\^/g, '$1');
                            return "(" + val + ")";
                        })
                        .join('|');

                /* The regex used to find emoji */
                return new RegExp(mega, "gi");
            }

            var defaultConfig = {
                blacklist: {
                    'ids': [],
                    'classes': ['no-emojify'],
                    'elements': ['script', 'style', 'a', 'pre', 'code']
                },
                tag_type: null,
                only_crawl_id: null,
                img_dir: 'images/emoji',
                ignore_emoticons: false,
                mode: 'img'
            };

            /* Returns true if the given char is whitespace */
            function isWhitespace(s) {
                return s === ' ' || s === '\t' || s === '\r' || s === '\n' || s === '' || s === String.fromCharCode(160);
            }

            var modeToElementTagType = {
                'img': 'img',
                'sprite': 'span',
                'data-uri': 'span'
            };

            /* Given a match in a node, replace the text with an image */
            function insertEmojicon(args) {
                var emojiElement = '';

                if(args.replacer){
                    emojiElement = args.replacer.apply({
                            config: defaultConfig
                        },
                        [args.emojiName, args.emojiName]
                    );
                }
                else {
                    var elementType = defaultConfig.tag_type || modeToElementTagType[defaultConfig.mode];
                    emojiElement = args.win.document.createElement(elementType);

                    if (elementType !== 'img') {
                        emojiElement.setAttribute('class', 'emoji emoji_' + args.emojiName);
                    } else {
                        //emojiElement.setAttribute('align', 'absmiddle');
                        emojiElement.setAttribute('alt', ':' + args.emojiName + ':');
                        emojiElement.setAttribute('class', 'emoji');
                        emojiElement.setAttribute('src', defaultConfig.img_dir + '/' + args.emojiName + '.png');
                    }

                    emojiElement.setAttribute('title', '[' + args.emojiName + ']');
                }
                var index = args.node.nodeValue.lastIndexOf(args.emojiText);
                args.node.splitText(index);
                args.node.nextSibling.nodeValue = args.node.nextSibling.nodeValue.substr(
                    args.emojiText.length, args.node.nextSibling.nodeValue.length
                );
                emojiElement.appendChild(args.node.splitText(index));
                args.node.parentNode.insertBefore(emojiElement, args.node.nextSibling);
            }

            /* Given an regex match, return the name of the matching emoji */
            function getEmojiNameForMatch(match) {
                /* Special case for named emoji */
                if(match[1] && match[2]) {
                    var named = match[2];
                    if(namedMatchHash[named]) { return named; }
                    return;
                }
                for(var i = 3; i < match.length - 1; i++) {
                    if(match[i]) {
                        return emoticonsProcessed[i - 2][1];
                    }
                }
            }

            function defaultReplacer(emoji, name) {
                /*jshint validthis: true */
                var elementType = this.config.tag_type || modeToElementTagType[this.config.mode];
                if (elementType !== 'img') {
                    return "<" +  elementType +" class='emoji emoji-" + name + "' title=':" + name + ":'></" + elementType+ ">";
                } else {
                    return "<img align='absmiddle' alt=':" + name + ":' class='emoji' src='" + this.config.img_dir + '/' + name + ".png' title=':" + name + ":' />";
                }
            }

            function Validator() {
                this.lastEmojiTerminatedAt = -1;
            }

            Validator.prototype = {
                validate: function(match, index, input) {
                    var self = this;

                    /* Validator */
                    var emojiName = getEmojiNameForMatch(match);
                    if(!emojiName) { return; }

                    var m = match[0];
                    var length = m.length;
                    // var index = match.index;
                    // var input = match.input;

                    function success() {
                        self.lastEmojiTerminatedAt = length + index;
                        return emojiName;
                    }

                    /* At the beginning? */
                    if(index === 0) { return success(); }

                    /* At the end? */
                    if(input.length === m.length + index) { return success(); }

                    var hasEmojiBefore = this.lastEmojiTerminatedAt === index;
                    if (hasEmojiBefore) { return success();}

                    /* Has a whitespace before? */
                    if(isWhitespace(input.charAt(index - 1))) { return success(); }

                    var hasWhitespaceAfter = isWhitespace(input.charAt(m.length + index));
                    /* Has a whitespace after? */
                    if(hasWhitespaceAfter && hasEmojiBefore) { return success(); }

                    return;
                }
            };

            function emojifyString (htmlString, replacer) {
                if(!htmlString) { return htmlString; }
                if(!replacer) { replacer = defaultReplacer; }

                emoticonsProcessed = initEmoticonsProcessed();
                emojiMegaRe = initMegaRe();

                var validator = new Validator();

                return htmlString.replace(emojiMegaRe, function() {
                    var matches = Array.prototype.slice.call(arguments, 0, -2);
                    var index = arguments[arguments.length - 2];
                    var input = arguments[arguments.length - 1];
                    var emojiName = validator.validate(matches, index, input);
                    if(emojiName) {
                        return replacer.apply({
                                config: defaultConfig
                            },
                            [arguments[0], emojiName]
                        );
                    }
                    /* Did not validate, return the original value */
                    return arguments[0];
                });

            }
            function run(el, replacer) {

                // Check if an element was not passed.
                // This will only work in the browser
                if(typeof el === 'undefined'){
                    // Check if an element was configured. If not, default to the body.
                    if (defaultConfig.only_crawl_id) {
                        el = document.getElementById(defaultConfig.only_crawl_id);
                    } else {
                        el = document.body;
                    }
                }

                // Get the window object from the passed element.
                var doc = el.ownerDocument,
                    win = doc.defaultView || doc.parentWindow;

                var treeTraverse = function (parent, cb){
                    var child;

                    if (parent.hasChildNodes()) {
                        child = parent.firstChild;
                        while(child){
                            if(cb(child)) {
                                treeTraverse(child, cb);
                            }
                            child = child.nextSibling;
                        }
                    }
                };

                var matchAndInsertEmoji = function(node) {
                    var match;
                    var matches = [];
                    var validator = new Validator();

                    while ((match = emojiMegaRe.exec(node.data)) !== null) {
                        if(validator.validate(match, match.index, match.input)) {
                            matches.push(match);
                        }
                    }

                    var findingMarches = function(data,emot){
                        var index = data.indexOf(emot[0]);
                        if(index > -1){
                            matches.push(emot);
                            findingMarches(data.substr(index+emot[0].length),emot);
                        }
                    }
                    for(var i = 0; i < emoticonsProcessed.length ; i++){
                        var emoticon = emoticonsProcessed[i];
                        //var index = node.data.indexOf(emoticon[0]);
                        //if(index > -1){
                        //    matches.push(emoticon);
                        //}
                        findingMarches(node.data,emoticon);
                    }

                    for (var i = 0; i < matches.length; i ++) {
                        /* Replace the text with the emoji */
                        //var emojiName = getEmojiNameForMatch(matches[i]);
                        var emoticon = matches[i];
                        insertEmojicon({
                            node: node,
                            //match: matches[i],
                            //emojiName: emojiName,
                            emojiText: emoticon[0],
                            emojiName: emoticon[1],
                            replacer: replacer,
                            win: win
                        });
                    }
                };

                emoticonsProcessed = initEmoticonsProcessed();
                emojiMegaRe = initMegaRe();

                var nodes = [];

                var elementsBlacklist = new RegExp(defaultConfig.blacklist.elements.join('|'), 'i'),
                    classesBlacklist = new RegExp(defaultConfig.blacklist.classes.join('|'), 'i');

                if(typeof win.document.createTreeWalker !== 'undefined') {
                    var nodeIterator = win.document.createTreeWalker(
                        el,
                        win.NodeFilter.SHOW_TEXT | win.NodeFilter.SHOW_ELEMENT,
                        function(node) {
                            if(node.nodeType !== 1) {
                                /* Text Node? Good! */
                                return win.NodeFilter.FILTER_ACCEPT;
                            }

                            if(node.tagName.match(elementsBlacklist) || node.tagName === "svg" || node.className.match(classesBlacklist)) {
                                return win.NodeFilter.FILTER_REJECT;
                            }

                            return win.NodeFilter.FILTER_SKIP;
                        },
                        false
                    );

                    var node;

                    while((node = nodeIterator.nextNode()) !== null) {
                        nodes.push(node);
                    }
                }
                else {
                    treeTraverse(el, function(node){
                        if(
                            (typeof node.tagName !== 'undefined' && node.tagName.match(elementsBlacklist)) ||
                            (typeof node.className !== 'undefined' && node.className.match(classesBlacklist))
                        ){
                            return false;
                        }
                        if (node.nodeType === 1) {
                            return true;
                        }

                        nodes.push(node);
                        return true;
                    });
                }

                nodes.forEach(matchAndInsertEmoji);
            }

            return {
                // Sane defaults
                defaultConfig: defaultConfig,
                emojiNames: namedEmoji,
                setConfig: function (newConfig) {
                    Object.keys(defaultConfig).forEach(function(f) {
                        if(f in newConfig) {
                            defaultConfig[f] = newConfig[f];
                        }
                    });
                },

                replace: emojifyString,

                // Main method
                run: run
            };
        })();

        return emojify;
    }
));
